CloudFormation을 이용해서 IaC 입문해보기
들어가며
안녕하세요! 클래스메서드 신입 엔지니어 임홍기입니다. 입사한지 벌써 3달이 되었네요.
오늘은 지금까지 공부해온 인프라 지식을 코드로 구성해보는 IaC에 대해서 알아보겠습니다.
해당 내용은 7월 1일 19시에 예정되어있는 Developers.IO Korea Online에서도 다룰 예정이니 관심 있으시면 참여 부탁드립니다!
목차
- IaC란?
- CloudFormation이란?
- CloudFormation을 이용해서 구성하는 방법
목차는 크게 3가지이며 왼쪽에 있는 목차화면을 통해 이동하는 것도 가능합니다!
IaC란?
IaC란 Infrastructure as Code의 약자이며, AWS의 환경이나 OS등의 인프라를 구성할 때
코드 기반으로 작성하여 관리하거나 갱신하는 방식입니다.
IaC의 배경
최근, 클라우드 기술의 발전에 의해 인프라 지식이 전무한 사람이라도
EC2 인스턴스 하나를 만드는 것쯤은 몇 번의 클릭만으로 해결할 수 있는 시대가 왔습니다! (저도 이 매력에 AWS에 빠지게 되었습니다 ㅎㅎ)
하지만, 구성해야 할 인프라 구조가 점점 복잡해지거나, 협업해야 할 때, 이전 구성에 대해 잊어버렸을 때는 어떻게 해야 할까요?
IaC는 이러한 문제의 해결을 위해 등장했습니다.
IaC의 이점
- 코드로 관리하기 때문에, 한눈에 모든 설정을 확인할 수 있습니다. (손으로 구성할 때 보다 실수가 적어짐)
- Github등의 버전 관리 서비스를 이용하면, 다른 사람과 협업하거나, 커밋로그로 변경을 관리할 수 있습니다.
- 템플릿으로 관리함으로써, 어떤 환경에서도 같은 구성으로 테스트할 수 있습니다.
- 템플릿으로 배포된 리소스들은 추적되며, 템플릿을 삭제하는 것만으로 생성된 모든 리소스를 완벽하게 삭제할 수 있습니다.
IaC의 단점
- 구성에 따라 다양한 툴들을 사용할 수 있어야 하여 러닝코스트가 높습니다. (사람 의존적)
- GUI 환경에서 클릭해서 설정하는 것이 더 빠를 수도 있습니다.
- IaC로 생성된 리소스는 기본적으로 IaC로 관리해야 하며, GUI에서 건드리면 안 됩니다. (스택 업데이트시 코드와 환경이 다르면 롤백이 발생)
CloudFormation이란?
코드(JSON/YAML)로 정의한 AWS의 리소스를 자동으로 프로비저닝 해주는 AWS의 툴이자 서비스입니다.
코드로 작성한 리소스 파일을 템플릿이라고 하며, 생성된 리소스를 스택이라고 부릅니다.
템플릿에 리소스를 정의해두면, 서로 연계되도록 구성되며, 삭제할때도 스택을 삭제하는 것으로 모든 리소스를 제거할 수 있습니다.
무엇보다, CloudFormation의 이용요금은 무료입니다.
CloudFormation으로 생성한 리소스의 사용량만큼 과금되는 것이 특징입니다. (매니지먼트 콘솔에서 생성한 리소스의 과금 방식과 동일)
CloudFormation의 동작 원리
- 코드(JSON/YAML)로 템플릿을 작성합니다.
- 정의한 템플릿을 S3에 업로드 합니다.
- 정의한 코드를 CloudFormation이 읽고
- 실행하는 것으로 정의한 환경이 AWS상에 구성됩니다.
CloudFormation 템플릿 구조
AWSTemplateFormatVersion: "version date" Description: String Metadata: template metadata Parameters: set of parameters Mappings: set of mappings Conditions: set of conditions Resources: set of resources Outputs: set of outputs
- AWSTemplateFormatVersion : 템플릿이 따르는 버전, 현재 2010-09-09 버전이 유일
- Description : 템플릿의 설정을 기술하는 부분, 항상 템플릿 버전 다음에 정의
- Parameters : 스택 생성 및 업데이트시 템플릿에 전달하는 값
- Mappings : 조건부 파라미터값을 지정하는 데 사용할 수 있는 Key-Value 값의 매핑 리전별로 리소스를 달리할 시 사용
- Resources : AWS 리소스 및 해당 리소스의 속성을 지정
- Outputs : 리소스 생성 후 받을 결과에 대해 정의
- Metadata : 템플릿에 대한 추가 정보
- Conditions : 스택 생성 또는 업데이트 시 특정 리소스 속성에 값이 할당되는지의 여부를 제어, 스택 환경이 prod인지 test인지에 따라 달라지는 리소스를 조건부로 생성할 때 사용
참고 : AWS CloudFormation 템플릿 구조
CloudFormation을 이용해서 구성하는 방법
2가지 패턴의 구성방법을 이용하여 실제로 구성해보도록 하겠습니다.
샘플 템플릿 이용
먼저 샘플 템플릿을 이용하여 빠르게 구성하는 방법을 알아보겠습니다.
매니지먼트 콘솔창에서 CloudFormation으로 이동한 뒤 Create stack에 With new resources를 선택합니다.
Use a sample template에서 구성해보고자하는 스택을 선택합니다.
여기선 LAMP Stack을 선택해서 구성해보겠습니다.
Stack name과 Parameters에 요구되는 값을 넣어주고 상세 설정을 한 뒤, Review 화면에서 설정한 템플릿을 확인하고 다음으로 넘어가는 것으로 샘플 템플릿을 이용한 구성은 끝이 납니다.
짧으면 수십초에서 수십분 기다리면 리소스들이 순서대로 생성됩니다.
정의한 스택 이름의 Status가 CREATE_COMPLETE가 되면 성공적으로 리소스들이 생성되었다는 의미입니다.
생성한 리소스들은 매니지먼트 콘솔창에서도 확인할 수 있습니다.
직접 템플릿 작성 및 업로드
이번에는 직접 템플릿을 작성하고, 업로드 하는 방법을 알아보겠습니다.
처음부터 작성하는 것도 나쁘지 않지만, 보통은 필요한 구성의 템플릿을 찾아서 수정하는 방법으로 작성을 많이 한다고 합니다!
이번에 작성한 예제는 다중 가용영역 LAMP스택 템플릿에 S3와 RDS를 추가로 정의해서 작성했습니다.
참고 템플릿
등을 이용해서 구성시 필요한 요소들을 적재적소로 사용하면 처음부터 작성하는 것 보다 편하게 작업하실 수 있습니다.
구성 환경 아키텍처
위의 구성대로 코드를 작성해 보았습니다.
작성한 코드는 코드량이 많은 관계로 가장 아래에 기재해 두었습니다.
CLI를 통한 업로드
작성한 yaml파일은 매니지먼트 콘솔에서 샘플 리소스를 선택한 것처럼 파일을 업로드해서 등록할 수 있지만, 이번엔 CLI 환경에서 템플릿을 업로드 해보겠습니다.
CLI 명령어
템플릿을 CLI를 통해 업로드 하기 위해서는, 매니지먼트 콘솔에서 파라미터값을 입력하듯이, 파라미터값을 넘겨줘야 합니다.
작성한 템플릿으로 스택을 생성하는 명령어
- --stack-name : 스택이름을 정의합니다.
- --template-body : 작성한 템플릿 파일명
- --parameters : 템플릿 내에 정의한 파라미터 값
- --capabilities : AWS 계정의 권한에 영향이 있는 리소스 생성시 사용(여기선 policy와 role을 작성하기 위해 정의)
쉘 스크립트 작성
예제로 작성한 코드는 위의 CLI명령으로 실행할 수 있으며
간단한 쉘 스크립트를 작성하여 실행되게 만들 수도 있습니다.
용도와 파라미터값에 따라 쉘 스크립트를 작성해서, 보다 빠르게 실행할 수 있습니다!
편의상 이번에는 스트링 값으로 입력했지만, DB Password의 설정은 Secrets Manager를 사용해서 arn을 넣어주는게 안전합니다.
생성된 리소스 확인
지금까지 위의 아키텍처대로 구성하고 실행했습니다.
약 20분에 걸쳐 정의한 리소스들과 정책이 생성된 것을 확인할 수 있습니다.
생성된 모든 리소스들은 매니지먼트 콘솔에서도 확인할 수 있습니다.
스택 삭제
생성된 스택은 아래의 명령어를 이용하여 생성하는 것과 같이 간단하게 삭제할 수 있습니다.
명령어
$ aws cloudformation delete-stack --stack-name [stackName]
CloudFormation에서 사용가능한 명령어는 CLI cloudformation reference를 참고해 주세요!
ha-lamp-stack.yml
# Template version AWSTemplateFormatVersion: 2010-09-09 Description: Create a HA, scalable LAMP stack with RDS, S3 # passed to parameter when stack is generated Parameters: VpcId: Type: "AWS::EC2::VPC::Id" Subnets: Type: "List<AWS::EC2::Subnet::Id>" KeyName: Type: "AWS::EC2::KeyPair::KeyName" BucketName: Type: String # Database parameter DBName: Default: myDatabase Type: String MinLength: "1" MaxLength: "64" AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" DBUser: NoEcho: "true" Description: Username for MySQL database access Type: String MinLength: "1" MaxLength: "16" AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*" DBPassword: NoEcho: "true" Description: Password for MySQL database access Type: String MinLength: "8" MaxLength: "41" AllowedPattern: "[a-zA-Z0-9]*" DBAllocatedStorage: Default: "5" Type: Number MinValue: "5" MaxValue: "1024" DBInstanceClass: Type: String Default: db.t2.micro # AllowedValues: # - db.t1.micro, db.t2.micro, db.t2.small, db.t2.medium, db.t2.large ... MultiAZDatabase: Default: "true" Type: String # AllowedValues: # - "true", "false" # AutoScaling EC2 size Default = 3 WebServerCapacity: Default: "3" Type: Number MinValue: "3" MaxValue: "10" InstanceType: Type: String Default: t2.micro # AllowedValues: # - t1.micro, t2.nano, t2.micro, t2.small, t2.medium, t2.large ... SSHLocation: Type: String MinLength: "9" MaxLength: "18" Default: 0.0.0.0/0 AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' Mappings: AWSInstanceType2Arch: t1.micro: Arch: HVM64 t2.nano: Arch: HVM64 t2.micro: Arch: HVM64 t2.small: Arch: HVM64 t2.medium: Arch: HVM64 t2.large: Arch: HVM64 AWSInstanceType2NATArch: t1.micro: Arch: NATHVM64 t2.nano: Arch: NATHVM64 t2.micro: Arch: NATHVM64 t2.small: Arch: NATHVM64 t2.medium: Arch: NATHVM64 t2.large: Arch: NATHVM64 AWSRegionArch2AMI: ap-northeast-1: HVM64: ami-00a5245b4816c38e6 HVMG2: ami-09d0e0e099ecabba2 ap-northeast-2: HVM64: ami-00dc207f8ba6dc919 HVMG2: NOT_SUPPORTED ap-northeast-3: HVM64: ami-0b65f69a5c11f3522 HVMG2: NOT_SUPPORTED Resources: # S3 Bucket S3Bucket: Type: AWS::S3::Bucket # DeletionPolicy: Retain 스택 삭제시 버킷 유지 Properties: BucketName: !Ref BucketName S3BucketRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: ["sts:AssumeRole"] Effect: Allow Principal: Service: [s3.amazonaws.com] BucketBackupPolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: ["s3:GetReplicationConfiguration", "s3:ListBucket"] Effect: Allow Resource: - !Join ["", ["arn:aws:s3:::", !Ref "S3Bucket"]] - Action: ["s3:GetObjectVersion", "s3:GetObjectVersionAcl"] Effect: Allow Resource: - !Join ["", ["arn:aws:s3:::", !Ref "S3Bucket", /*]] - Action: ["s3:ReplicateObject", "s3:ReplicateDelete"] Effect: Allow Resource: - !Join [ "", [ "arn:aws:s3:::", !Join [ "-", [ !Ref "AWS::Region", !Ref "AWS::StackName", replicationbucket, ], ], /*, ], ] PolicyName: BucketBackupPolicy Roles: [!Ref "S3BucketRole"] ApplicationLoadBalancer: Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" Properties: Subnets: !Ref Subnets ALBListener: Type: "AWS::ElasticLoadBalancingV2::Listener" Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref ALBTargetGroup LoadBalancerArn: !Ref ApplicationLoadBalancer Port: "80" Protocol: HTTP ALBTargetGroup: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: HealthCheckIntervalSeconds: 10 HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 Port: 80 Protocol: HTTP UnhealthyThresholdCount: 5 VpcId: !Ref VpcId TargetGroupAttributes: - Key: stickiness.enabled Value: "true" - Key: stickiness.type Value: lb_cookie - Key: stickiness.lb_cookie.duration_seconds Value: "30" WebServerGroup: Type: "AWS::AutoScaling::AutoScalingGroup" Properties: VPCZoneIdentifier: !Ref Subnets LaunchConfigurationName: !Ref LaunchConfig DesiredCapacity: !Ref WebServerCapacity TargetGroupARNs: - !Ref ALBTargetGroup CreationPolicy: ResourceSignal: Timeout: PT5M Count: !Ref WebServerCapacity UpdatePolicy: AutoScalingRollingUpdate: MinInstancesInService: "1" MaxBatchSize: "1" PauseTime: PT15M WaitOnResourceSignals: "true" # EC2 Auto Scaling 시작 구성 LaunchConfig: Type: "AWS::AutoScaling::LaunchConfiguration" Metadata: Comment1: Configure the bootstrap helpers to install the Apache Web Server and PHP Comment2: >- The website content is downloaded from the CloudFormationPHPSample.zip file "AWS::CloudFormation::Init": config: packages: yum: httpd: [] php: [] php-mysql: [] files: /var/www/html/index.php: content: !Join - "" - - | <html> - |2 <head> - |2 <title>AWS CloudFormation PHP Sample</title> - |2 <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> - |2 </head> - |2 <body> - |2 <h1>Welcome to the AWS CloudFormation PHP Sample</h1> - |2 <p/> - |2 <?php - |2 // Print out the current data and tie - |2 print "The Current Date and Time is: <br/>"; - |2 print date("g:i A l, F j Y."); - |2 ?> - |2 <p/> - |2 <?php - |2 // Setup a handle for CURL - |2 $curl_handle=curl_init(); - |2 curl_setopt($curl_handle,CURLOPT_CONNECTTIMEOUT,2); - |2 curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER,1); - |2 // Get the hostname of the intance from the instance metadata - |2 curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/public-hostname'); - |2 $hostname = curl_exec($curl_handle); - |2 if (empty($hostname)) - |2 { - |2 print "Sorry, for some reason, we got no hostname back <br />"; - |2 } - |2 else - |2 { - |2 print "Server = " . $hostname . "<br />"; - |2 } - |2 // Get the instance-id of the intance from the instance metadata - |2 curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/instance-id'); - |2 $instanceid = curl_exec($curl_handle); - |2 if (empty($instanceid)) - |2 { - |2 print "Sorry, for some reason, we got no instance id back <br />"; - |2 } - |2 else - |2 { - |2 print "EC2 instance-id = " . $instanceid . "<br />"; - |2 } - ' $Database = "' - !GetAtt - MySQLDatabase - Endpoint.Address - | "; - ' $DBUser = "' - !Ref DBUser - | "; - ' $DBPassword = "' - !Ref DBPassword - | "; - |2 print "Database = " . $Database . "<br />"; - |2 $dbconnection = mysql_connect($Database, $DBUser, $DBPassword) - |2 or die("Could not connect: " . mysql_error()); - |2 print ("Connected to $Database successfully"); - |2 mysql_close($dbconnection); - |2 ?> - |2 <h2>PHP Information</h2> - |2 <p/> - |2 <?php - |2 phpinfo(); - |2 ?> - |2 </body> - | </html> mode: "000600" owner: apache group: apache /etc/cfn/cfn-hup.conf: content: !Join - "" - - | [main] - stack= - !Ref "AWS::StackId" - |+ - region= - !Ref "AWS::Region" - |+ mode: "000400" owner: root group: root /etc/cfn/hooks.d/cfn-auto-reloader.conf: content: !Join - "" - - | [cfn-auto-reloader-hook] - | triggers=post.update - > path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init - "action=/opt/aws/bin/cfn-init -v " - " --stack " - !Ref "AWS::StackName" - " --resource LaunchConfig " - " --region " - !Ref "AWS::Region" - |+ - | runas=root mode: "000400" owner: root group: root services: sysvinit: httpd: enabled: "true" ensureRunning: "true" cfn-hup: enabled: "true" ensureRunning: "true" files: - /etc/cfn/cfn-hup.conf - /etc/cfn/hooks.d/cfn-auto-reloader.conf Properties: ImageId: !FindInMap - AWSRegionArch2AMI - !Ref "AWS::Region" - !FindInMap - AWSInstanceType2Arch - !Ref InstanceType - Arch InstanceType: !Ref InstanceType SecurityGroups: - !Ref WebServerSecurityGroup KeyName: !Ref KeyName UserData: !Base64 "Fn::Join": - "" - - | #!/bin/bash -xe - | yum update -y aws-cfn-bootstrap - | # Install the files and packages from the metadata - "/opt/aws/bin/cfn-init -v " - " --stack " - !Ref "AWS::StackName" - " --resource LaunchConfig " - " --region " - !Ref "AWS::Region" - |+ - | # Signal the status from cfn-init - "/opt/aws/bin/cfn-signal -e $? " - " --stack " - !Ref "AWS::StackName" - " --resource WebServerGroup " - " --region " - !Ref "AWS::Region" - |+ # SecurityGroup WebServerSecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: Enable HTTP access via port 80 locked down to the ELB and SSH access SecurityGroupIngress: - IpProtocol: tcp FromPort: "80" ToPort: "80" SourceSecurityGroupId: !Select - 0 - !GetAtt - ApplicationLoadBalancer - SecurityGroups - IpProtocol: tcp FromPort: "22" ToPort: "22" CidrIp: !Ref SSHLocation VpcId: !Ref VpcId DBEC2SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: Open database for access SecurityGroupIngress: - IpProtocol: tcp FromPort: "3306" ToPort: "3306" SourceSecurityGroupId: !Ref WebServerSecurityGroup VpcId: !Ref VpcId # Database MySQLDatabase: Type: "AWS::RDS::DBInstance" Properties: Engine: MySQL DBName: !Ref DBName MultiAZ: !Ref MultiAZDatabase MasterUsername: !Ref DBUser MasterUserPassword: !Ref DBPassword DBInstanceClass: !Ref DBInstanceClass AllocatedStorage: !Ref DBAllocatedStorage VPCSecurityGroups: - !GetAtt - DBEC2SecurityGroup - GroupId Outputs: WebsiteURL: Description: URL for newly created LAMP stack Value: !Join - "" - - "http://" - !GetAtt - ApplicationLoadBalancer - DNSName
참고
Troubleshooting
CloudFormation create-stack reference
CloudFormation CLI Command Reference
스택 리소스 삭제 및 업데이트 방지
스택 삭제시 리소스 유지